/*
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * This file is a derivative of code released under the terms listed below.
 *
 */
/*
*  Copyright (c) 2013,
*      Tobias Blaschke <code@tobiasblaschke.de>
*  All rights reserved.

*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions are met:
*
*  1. Redistributions of source code must retain the above copyright notice,
*     this list of conditions and the following disclaimer.
*
*  2. Redistributions in binary form must reproduce the above copyright notice,
*     this list of conditions and the following disclaimer in the documentation
*     and/or other materials provided with the distribution.
*
*  3. The names of the contributors may not be used to endorse or promote
*     products derived from this software without specific prior written
*     permission.
*
*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
*  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
*  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
*  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*  POSSIBILITY OF SUCH DAMAGE.
*/
package com.ibm.wala.dalvik.ipa.callgraph.androidModel.parameters;

import com.ibm.wala.analysis.typeInference.ConeType;
import com.ibm.wala.analysis.typeInference.PrimitiveType;
import com.ibm.wala.analysis.typeInference.TypeAbstraction;
import com.ibm.wala.classLoader.ArrayClass;
import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.NewSiteReference;
import com.ibm.wala.ipa.callgraph.AnalysisScope;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ipa.summaries.VolatileMethodSummary;
import com.ibm.wala.shrikeBT.IInvokeInstruction;
import com.ibm.wala.ssa.ConstantValue;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.ssa.IInstantiator;
import com.ibm.wala.util.ssa.ParameterAccessor;
import com.ibm.wala.util.ssa.SSAValue;
import com.ibm.wala.util.ssa.SSAValue.UniqueKey;
import com.ibm.wala.util.ssa.SSAValue.VariableKey;
import com.ibm.wala.util.ssa.SSAValueManager;
import com.ibm.wala.util.ssa.TypeSafeInstructionFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Add code to create an instance of a type in a synthetic method.
 *
 * <p>This variant limits recursion depth.
 *
 * @author Tobias Blaschke &lt;code@tobiasblaschke.de&gt;
 */
public class FlatInstantiator implements IInstantiator {
  private static final Logger logger = LoggerFactory.getLogger(FlatInstantiator.class);

  final IClassHierarchy cha;
  final VolatileMethodSummary body;
  final TypeSafeInstructionFactory instructionFactory;
  final SSAValueManager pm;
  final MethodReference scope;
  final AnalysisScope analysisScope;
  final int maxDepth;

  public FlatInstantiator(
      final VolatileMethodSummary body,
      final TypeSafeInstructionFactory instructionFactory,
      final SSAValueManager pm,
      final IClassHierarchy cha,
      final MethodReference scope,
      final AnalysisScope analysisScope) {
    this.body = body;
    this.instructionFactory = instructionFactory;
    this.pm = pm;
    this.cha = cha;
    this.scope = scope;
    this.analysisScope = analysisScope;
    this.maxDepth = 1;
  }

  public FlatInstantiator(
      final VolatileMethodSummary body,
      final TypeSafeInstructionFactory instructionFactory,
      final SSAValueManager pm,
      final IClassHierarchy cha,
      final MethodReference scope,
      final AnalysisScope analysisScope,
      final int maxDepth) {
    this.body = body;
    this.instructionFactory = instructionFactory;
    this.pm = pm;
    this.cha = cha;
    this.scope = scope;
    this.analysisScope = analysisScope;
    this.maxDepth = maxDepth;
  }

  private boolean isExcluded(IClass cls) {
    if (this.analysisScope.getExclusions() != null
        && this.analysisScope.getExclusions().contains(cls.getName().toString())) { // XXX FUUUUU
      logger.info("Hit exclusions with {}", cls);
      return true;
    } else {
      return false;
    }
  }

  /**
   * Creates a new instance of type calling all that's necessary.
   *
   * <p>If T is a class-type all its constructors are searched for the one found best suited (takes
   * the least arguments, ...). New instances are created for all parameters, then the constructor
   * is called.
   *
   * <p>If T represents multiple types (is an interface, abstract class, ...) _all_ implementors of
   * that type are instantiated After that they get Phi-ed together.
   *
   * <p>If T is an array-type a new array of length 1 is generated.
   *
   * <p>TODO: Do we want to mix in REUSE-Parameters?
   */
  public SSAValue createInstance(
      final TypeReference T,
      final boolean asManaged,
      VariableKey key,
      Set<? extends SSAValue> seen) {
    return createInstance(T, asManaged, key, seen, 0);
  }

  private SSAValue createInstance(
      final TypeReference T,
      final boolean asManaged,
      VariableKey key,
      Set<? extends SSAValue> seen,
      int currentDepth) {
    if (T == null) {
      throw new IllegalArgumentException("Can't create an instance of null");
    }
    if (seen == null) {
      logger.debug("Empty seen");
      seen = new HashSet<>();
    }

    if (currentDepth > this.maxDepth) {
      final SSAValue instance = this.pm.getUnmanaged(T, key);
      instance.setAssigned();
      return instance;
    }

    { // Special type?
      final SpecializedInstantiator sInst =
          new SpecializedInstantiator(
              body, instructionFactory, pm, cha, scope, analysisScope, this);
      if (SpecializedInstantiator.understands(T)) {
        return sInst.createInstance(T, asManaged, key, seen, currentDepth);
      }
    }

    final IClass klass = this.cha.lookupClass(T);
    final SSAValue instance;
    { // fetch new value
      if (asManaged) {
        if (key == null) {
          throw new IllegalArgumentException("A managed variable needs a key - null given.");
        }
        if ((klass != null) && (klass.isAbstract() || klass.isInterface())) {
          // We'll need a phi
          instance = this.pm.getFree(T, key);
        } else {
          instance = this.pm.getUnallocated(T, key);
        }
      } else {
        if (key == null) {
          key = new UniqueKey();
        }
        instance = this.pm.getUnmanaged(T, key);
      }
    }

    if (T.isPrimitiveType()) {
      createPrimitive(instance);
      return instance;
    } else if (klass == null) {
      if (!T.getName().toString().startsWith("Landroid/")) {
        logger.error("The Type {} is not in the ClassHierarchy! Returning null as instance", T);
      } else {
        logger.debug("The Type {} is not in the ClassHierarchy! Returning null as instance", T);
      }
      this.body.addConstant(instance.getNumber(), new ConstantValue(null));
      instance.setAssigned();
      return instance;
    } else if (isExcluded(klass)) {
      this.body.addConstant(
          instance.getNumber(), new ConstantValue(null)); // TODO: null or nothing?
      instance.setAssigned();
      return instance;
    }

    final Set<TypeReference> types = getTypes(T);

    logger.info("Creating instance of {} is  {}", T, types);
    if (types.isEmpty()) {
      throw new IllegalStateException("Types of " + T + " are empty");
    }

    if ((types.size() == 1)
        && (!klass.isAbstract())
        && (!klass.isArrayClass())
        && (!klass.isInterface())) {
      // It's a "regular" class
      final SSANewInstruction newInst = addNew(instance);
      selectAndCallCtor(instance, seen, currentDepth);
      if (asManaged) {
        this.pm.setAllocation(instance, newInst);
      }
      assert (newInst.getDef() == instance.getNumber());
      return instance;
    } else if (klass.isArrayClass()) {
      logger.info("Creating Array-Class {}", klass.toString());

      final TypeReference payloadType = T.getArrayElementType();
      SSAValue payload = null;
      {
        for (final SSAValue see : seen) {
          if (ParameterAccessor.isAssignable(see.getType(), payloadType, this.cha)) {
            // Happens on Array of interfaces
            logger.trace("Reusing {} for array payload {}", see, payload);
            payload = see;
          }
        }
        if (payload == null) {
          payload = createInstance(payloadType, false, new UniqueKey(), seen, currentDepth);
        }
      }
      // assert (types.size() == 1);   // TODO

      // Generate an array of length 1
      final SSANewInstruction newInst;
      {
        final int pc = this.body.getNextProgramCounter();
        final NewSiteReference nRef = NewSiteReference.make(pc, instance.getType());

        final SSAValue arrayLength = this.pm.getUnmanaged(TypeReference.Int, new UniqueKey());
        this.body.addConstant(arrayLength.getNumber(), new ConstantValue(1));
        arrayLength.setAssigned();

        final ArrayList<SSAValue> params = new ArrayList<>(1);
        params.add(arrayLength);

        newInst = this.instructionFactory.NewInstruction(pc, instance, nRef, params);
        this.body.addStatement(newInst);
        assert (instance.getNumber() == newInst.getDef());
      }

      // Put a payload into the array
      {
        final int pc = this.body.getNextProgramCounter();
        final SSAInstruction write =
            this.instructionFactory.ArrayStoreInstruction(pc, instance, 0, payload);
        body.addStatement(write);
      }

      assert (newInst.getDef() == instance.getNumber());
      return instance;
    } else {
      // Abstract, Interface or array
      logger.debug("Not a regular class {}", T);
      final Set<SSAValue> subInstances = new HashSet<>();
      for (final TypeReference type : types) {
        final IClass subKlass = this.cha.lookupClass(type);

        if (subKlass.isAbstract() || subKlass.isInterface()) {
          // All "regular" classes in consideration should already be in types
          continue;
        }

        { // Create instance of subInstance
          final SSAValue subInstance = pm.getUnmanaged(type, new UniqueKey());
          final SSANewInstruction newInst = addNew(subInstance);
          selectAndCallCtor(subInstance, seen, currentDepth);
          assert (subInstance.getNumber() == newInst.getDef())
              : "Unexpected: number and def differ: "
                  + subInstance.getNumber()
                  + ", "
                  + newInst.getDef();
          final Set<SSAValue> newSeen = new HashSet<>(seen); // Narf
          newSeen.add(subInstance);
          seen = newSeen;
          subInstances.add(subInstance);
        }
      }

      TypeAbstraction abstraction = null;
      { // Build the abstraction
        for (final TypeReference type : types) {
          final IClass cls;
          if (type.isPrimitiveType()) {
            cls = null;
          } else {
            cls = this.cha.lookupClass(type);
            assert (cls != null);
          }

          if (abstraction == null) {
            // TODO: assert primitive stays primitive
            if (type.isPrimitiveType()) { // XXX: May this happen here?
              abstraction = PrimitiveType.getPrimitive(type);
            } else {
              abstraction = new ConeType(cls);
            }
          } else {
            if (type.isPrimitiveType()) {
              abstraction = abstraction.meet(PrimitiveType.getPrimitive(type));
            } else {
              abstraction = abstraction.meet(new ConeType(cls));
            }
          }
        }

        // XXX: What to do with the abstraction now?
      }

      { // Phi together everything
        if (subInstances.size() > 0) {
          final int pc = this.body.getNextProgramCounter();
          final SSAInstruction phi =
              this.instructionFactory.PhiInstruction(pc, instance, subInstances);
          body.addStatement(phi);
          if (asManaged) {
            this.pm.setPhi(instance, phi);
          }
        } else {
          logger.warn("No sub-instances for: {} - setting to null", instance);
          this.body.addConstant(instance.getNumber(), new ConstantValue(null));
          instance.setAssigned();
        }
      }
    }

    return instance;
  }

  private static void createPrimitive(SSAValue instance) {
    // XXX; something else?
    instance.setAssigned();
  }

  /** Add a NewInstruction to the body. */
  private SSANewInstruction addNew(SSAValue val) {
    final int pc = this.body.getNextProgramCounter();
    final NewSiteReference nRef = NewSiteReference.make(pc, val.getType());
    final SSANewInstruction newInstr = this.instructionFactory.NewInstruction(pc, val, nRef);
    this.body.addStatement(newInstr);
    assert (val.getNumber() == newInstr.getDef());
    return newInstr;
  }

  //  /**
  //   * Add a call to a single clinit to the body.
  //   *
  //   * @param val the "this" to call clinit on
  //   * @param inClass the class to call its clinit of
  //   */
  /*private void addCallCLinit(SSAValue val, TypeReference inClass) {
      final int pc = this.body.getNextProgramCounter();
      final MethodReference mRef = MethodReference.findOrCreate(inClass, MethodReference.clinitSelector);
      final SSAValue exception = pm.getException();
      final CallSiteReference site = CallSiteReference.make(pc, mRef, IInvokeInstruction.Dispatch.STATIC);
      final List<SSAValue> params = new ArrayList<SSAValue>(1);
      params.add(val);
      final SSAInstruction ctorCall = instructionFactory.InvokeInstruction(pc, params, exception, site);
      body.addStatement(ctorCall);
  } */

  /**
   * Add a call to the given constructor to the body.
   *
   * @param self the "this" to call the constructor on
   * @param ctor the constructor to call
   * @param ctorParams parameters to the ctor _without_ implicit this
   */
  protected void addCallCtor(SSAValue self, MethodReference ctor, List<SSAValue> ctorParams) {
    final int pc = this.body.getNextProgramCounter();
    final SSAValue exception = pm.getException();
    final CallSiteReference site =
        CallSiteReference.make(pc, ctor, IInvokeInstruction.Dispatch.SPECIAL);
    final List<SSAValue> params = new ArrayList<>(1 + ctorParams.size());
    params.add(self);
    params.addAll(ctorParams);
    final SSAInstruction ctorCall =
        instructionFactory.InvokeInstruction(pc, params, exception, site);
    body.addStatement(ctorCall);
  }

  private MethodReference selectAndCallCtor(
      SSAValue val, final Set<? extends SSAValue> overrides, final int currentDepth) {
    final IMethod cTor = lookupConstructor(val.getType());
    final ParameterAccessor ctorAcc = new ParameterAccessor(cTor);
    assert (ctorAcc.hasImplicitThis()) : "CTor detected as not having implicit this pointer";
    logger.debug("Acc for: {}", this.scope);
    final ParameterAccessor acc =
        new ParameterAccessor(this.scope, false); // TODO pm needs a connectThrough too!
    // TODO false is false
    // TODO: The overrides may lead to use before definition
    // if (acc.firstOf(val.getType().getName()) != null) {
    final SSAValue nullSelf = pm.getUnmanaged(val.getType(), new UniqueKey());
    this.body.addConstant(nullSelf.getNumber(), new ConstantValue(null));
    nullSelf.setAssigned();
    // }
    final Set<SSAValue> seen = new HashSet<>(1 + overrides.size());
    seen.add(nullSelf);
    seen.addAll(overrides);

    logger.debug("Recursing for: {}", cTor);
    logger.debug("With seen: {}", seen);
    final List<SSAValue> ctorParams =
        acc.connectThrough(
            ctorAcc,
            overrides, /* defaults */
            null,
            this.cha,
            this, /* managed */
            false, /* key */
            null,
            seen,
            currentDepth + 1); // XXX This starts the recursion!
    addCallCtor(val, cTor.getReference(), ctorParams);
    return cTor.getReference();
  }

  /**
   * Get all sub-types a type represents until concrete ones are reached.
   *
   * <p>A concrete type only represents itself.
   *
   * @throws IllegalArgumentException if T is a primitive.
   */
  private Set<TypeReference> getTypes(final TypeReference T) {
    final IClass cls = this.cha.lookupClass(T);
    if (isExcluded(cls)) {
      return new HashSet<>();
    }
    return getTypes(T, Collections.<TypeReference>emptySet());
  }

  /** Used internally to avoid endless recursion on getTypes(). */
  private Set<TypeReference> getTypes(final TypeReference T, final Set<TypeReference> seen) {
    logger.debug("getTypes({}, {})", T, seen);
    final Set<TypeReference> ret = new HashSet<>();
    ret.add(T);

    if (T.isPrimitiveType()) {
      logger.warn("getTypes called on a primitive");
      return ret;
      // throw new IllegalArgumentException("Not you that call primitive type on :P");
    }

    final IClass cls = this.cha.lookupClass(T);
    if (cls == null) {
      logger.error("The type {} is not in the ClassHierarchy - try continuing anyway", T);
      return ret;
      // throw new IllegalArgumentException("The type " + T + " is not in the ClassHierarchy");
    } else if (isExcluded(cls)) {
      return ret;
    } else if (seen.contains(T)) {
      return ret;
    }

    if (cls.isInterface()) {
      final Set<IClass> impls = cha.getImplementors(T);
      if (impls.isEmpty()) {
        // throw new IllegalStateException("The interface " + T + " has no known implementors");
        if (!T.getName().toString().startsWith("Landroid/")) {
          logger.error("The interface {} has no known implementors - skipping over it", T);
        } else {
          logger.debug("The interface {} has no known implementors - skipping over it", T);
        }
        return ret; // XXX: This is a bad idea?
      } else {
        // ADD all
        for (IClass impl : impls) {
          if (impl.isAbstract()) {
            ret.addAll(getTypes(impl.getReference(), ret)); // impl added through recursion
          } else {
            ret.add(impl.getReference());
          }
        }
      }
    } else if (cls.isAbstract()) {
      final Collection<IClass> subs = cha.computeSubClasses(T);
      if (subs.isEmpty()) {
        throw new IllegalStateException(
            "The class " + T + " is abstract but has no subclasses known to the ClassHierarchy");
      } else {
        for (final IClass sub : subs) {
          if (seen.contains(sub.getReference())) {
            logger.debug("Seen: {}", sub);
            continue;
          }
          if (sub.isAbstract()) {
            // Recurse on abstract classes
            ret.addAll(getTypes(sub.getReference(), ret)); // sub added through recursion
          } else {
            ret.add(sub.getReference());
          }
        }
      }
    } else if (cls.isArrayClass()) {
      final ArrayClass aCls = (ArrayClass) cls;
      final int dim = aCls.getDimensionality();

      if (aCls.isOfPrimitives()) {
        ret.add(aCls.getReference());
      } else {
        final IClass inner = aCls.getInnermostElementClass();

        if (inner == null) {
          throw new IllegalStateException("The array " + T + " has no inner class");
        }

        if ((inner.isInterface()) || (inner.isAbstract())) {
          final Set<TypeReference> innerTypes =
              getTypes(inner.getReference(), Collections.<TypeReference>emptySet());
          for (TypeReference iT : innerTypes) {
            TypeReference aT = TypeReference.findOrCreateArrayOf(iT);
            for (int i = 1; i < dim; ++i) {
              aT = TypeReference.findOrCreateArrayOf(aT);
            }
            ret.add(aT);
          }
        } else {
          ret.add(TypeReference.findOrCreateArrayOf(inner.getReference()));
        }
      }
    }

    return ret;
  }

  // /** Path back to Object (including T itself). */
  //    private List<TypeReference> getAllSuper(final TypeReference T) {
  //        if (T.isPrimitiveType()) {
  //            throw new IllegalArgumentException("Not you that call primitive type on :P");
  //        }
  //        final List<TypeReference> ret = new ArrayList<TypeReference>();
  //
  //        IClass cls = this.cha.lookupClass(T);
  //        if (cls == null) {
  //            throw new IllegalArgumentException("The type " + T + " is not in the
  // ClassHierarchy");
  //        }
  //
  //        while (cls != null) {
  //            ret.add(cls.getReference());
  //            cls = cls.getSuperclass();
  //        }
  //
  //        return ret;
  //    }

  // /** The Constructor starts with 'this()' or 'super()'. */
  /*private boolean callsCtor(MethodReference ctor) {
      if (ctor == null) {
          throw new IllegalArgumentException("Null ctor");
      }

      final Set<IMethod> methods = cha.getPossibleTargets(ctor);

      if (methods == null) {
          throw new IllegalArgumentException("Unable to look up IMethod for ctor " + ctor);
      }

      if (methods.size() != 1) {
          throw new UnsupportedOperationException("Unexpected multiple candidates for ctor " + ctor + " are " + methods);
      }

      final IMethod method = methods.iterator().next();
      assert (method.isInit());
      final SSAInstruction firstInstruction = this.cache.getIR(method).iterateAllInstructions().next();
      logger.debug("First instruction of ctor is: " + firstInstruction);
      if (firstInstruction instanceof SSAAbstractInvokeInstruction) {
          final SSAAbstractInvokeInstruction invokation = (SSAAbstractInvokeInstruction) firstInstruction;
          return invokation.isSpecial(); // Always?
      }

      return false;
  }*/

  /** Selects the constructor of T found to be bes suited. */
  private IMethod lookupConstructor(TypeReference T) {
    IMethod ctor = null;
    int score = -10000;
    final IClass klass = cha.lookupClass(T);

    if (klass == null) {
      throw new IllegalArgumentException("Unable to look up the class for " + T);
    }

    if (klass.isInterface() || klass.isAbstract()) {
      throw new IllegalArgumentException("Class is interface or abstract");
    }

    for (final IMethod im : klass.getDeclaredMethods()) {
      if (!im.isInit()) continue;

      int candidScore = 0;
      final int paramCount = im.getNumberOfParameters();

      if (im.isPrivate()) {
        score -= 10;
      } else if (im.isProtected()) {
        score -= 1;
      }

      for (int i = 1; i < paramCount; ++i) {
        final TypeReference paramType = im.getParameterType(i);

        if (paramType.isPrimitiveType()) {
          candidScore -= 1;
        } else if (paramType.isArrayType()) { // TODO: Reevaluate scores
          candidScore -= 30;

          if (paramType.getInnermostElementType().equals(T)) {
            // Array of itself
            candidScore -= 1000;
          }
        } else if (paramType.isClassType()) {
          candidScore -= 101;
        } else if (paramType.isReferenceType()) { // TODO: Avoid interfaces
          if (paramType.equals(T)) {
            candidScore -= 1000;
          } else {
            candidScore -= 7;
          }

          if (paramType.equals(TypeReference.JavaLangObject)) {
            candidScore -= 1500;
          }
        } else {
          // ?!
          candidScore -= 800;
        }
      }

      if (candidScore > score) {
        ctor = im;
        score = candidScore;
      }

      logger.debug("CTor {} got score {}", im, candidScore);
    }

    if (ctor == null) {
      logger.warn("Still found no CTor for {}", T);
      return cha.resolveMethod(klass, MethodReference.initSelector);
    } else {
      return ctor;
    }
  }

  /** Satisfy the interface. */
  @Override
  @SuppressWarnings("unchecked")
  public int createInstance(TypeReference type, Object... instantiatorArgs) {
    // public SSAValue createInstance(final TypeReference T, final boolean asManaged, VariableKey
    // key, Set<SSAValue> seen) {
    if (!(instantiatorArgs[0] instanceof Boolean)) {
      throw new IllegalArgumentException("Argument 0 to createInstance has to be boolean.");
    }
    if (!((instantiatorArgs[1] == null) || (instantiatorArgs[1] instanceof VariableKey))) {
      throw new IllegalArgumentException(
          "Argument 1 to createInstance has to be null or an instance of VariableKey");
    }
    if (!((instantiatorArgs[2] == null) || (instantiatorArgs[2] instanceof Set))) {
      throw new IllegalArgumentException(
          "Argument 2 to createInstance has to be null or an instance of Set<? extends SSAValue>, "
              + "got: "
              + instantiatorArgs[2].getClass());
    }
    final int currentDepth;
    {
      if (instantiatorArgs.length == 4) {
        currentDepth = (Integer) instantiatorArgs[3];
      } else {
        currentDepth = 0;
      }
    }
    if (instantiatorArgs[2] != null) {
      final Set<?> seen = (Set<?>) instantiatorArgs[2];
      if (!seen.isEmpty()) {
        final Object o = seen.iterator().next();
        if (!(o instanceof SSAValue)) {
          throw new IllegalArgumentException(
              "Argument 2 to createInstance has to be null or an instance of Set<? extends SSAValue>, "
                  + "got Set<"
                  + o.getClass()
                  + '>');
        }
      }
    }

    return createInstance(
            type,
            (Boolean) instantiatorArgs[0],
            (VariableKey) instantiatorArgs[1],
            (Set<? extends SSAValue>) instantiatorArgs[2],
            currentDepth)
        .getNumber();
  }
}
